use std::time::{Duration, Instant};

use futures::stream::{FuturesUnordered, StreamExt};
use serde::Serialize;
use tokio::{net::TcpStream, process::Command, time};
use trust_dns_resolver::{config::*, TokioAsyncResolver};

#[cfg(windows)]
use encoding_rs::GBK;

#[derive(Debug, Serialize, Clone)]
pub struct ProbeEnvelope<T> {
    pub target: String,
    pub success: bool,
    pub elapsed_ms: u128,
    pub data: Option<T>,
    pub error: Option<String>,
}

#[derive(Debug, Serialize, Clone)]
pub struct PingAttempt {
    pub seq: u32,
    pub success: bool,
    pub rtt_ms: Option<f64>,
    pub message: String,
}

#[derive(Debug, Serialize, Clone)]
pub struct PingReport {
    pub count: u32,
    pub transmitted: u32,
    pub received: u32,
    pub attempts: Vec<PingAttempt>,
    pub stdout: String,
}

#[derive(Debug, Serialize, Clone)]
pub struct TcpingAttempt {
    pub seq: u32,
    pub success: bool,
    pub rtt_ms: Option<f64>,
    pub error: Option<String>,
}

#[derive(Debug, Serialize, Clone)]
pub struct TcpingReport {
    pub target: String,
    pub port: u16,
    pub count: u32,
    pub attempts: Vec<TcpingAttempt>,
}

#[derive(Debug, Serialize, Clone)]
pub struct DnsRecord {
    pub record_type: String,
    pub value: String,
}

#[derive(Debug, Serialize, Clone)]
pub struct DnsReport {
    pub query: String,
    pub records: Vec<DnsRecord>,
}

#[derive(Debug, Serialize, Clone)]
pub struct PortStatus {
    pub port: u16,
    pub open: bool,
    pub elapsed_ms: u128,
}

#[derive(Debug, Serialize, Clone)]
pub struct PortscanReport {
    pub target: String,
    pub ports: Vec<PortStatus>,
    pub open_ports: Vec<u16>,
}

pub struct PingOptions<'a> {
    pub target: &'a str,
    pub count: u32,
    pub timeout: Duration,
}

pub struct TcpingOptions<'a> {
    pub target: &'a str,
    pub port: u16,
    pub count: u32,
    pub timeout: Duration,
}

pub struct DnsOptions<'a> {
    pub query: &'a str,
    pub record_type: &'a str,
}

pub struct PortscanOptions<'a> {
    pub target: &'a str,
    pub ports: Vec<u16>,
    pub timeout: Duration,
    pub concurrency: usize,
}

fn sanitize_target(target: &str) -> String {
    target.trim().to_string()
}

pub async fn ping(options: PingOptions<'_>) -> ProbeEnvelope<PingReport> {
    let target = sanitize_target(options.target);
    let start = Instant::now();

    #[cfg(windows)]
    let args = vec![
        "-n".to_string(),
        options.count.to_string(),
        "-w".to_string(),
        options.timeout.as_millis().to_string(),
        target.clone(),
    ];

    #[cfg(not(windows))]
    let args = vec![
        "-c".to_string(),
        options.count.to_string(),
        "-W".to_string(),
        options.timeout.as_secs().to_string(),
        target.clone(),
    ];

    let output = Command::new("ping").args(args.iter()).output().await;

    match output {
        Ok(output) => {
            let stdout = decode_output(&output.stdout);
            let stderr = decode_output(&output.stderr);
            let success = output.status.success();
            let attempts = parse_ping_output(&stdout, options.count);
            let received = attempts.iter().filter(|a| a.success).count() as u32;
            let transmitted = options.count;

            ProbeEnvelope {
                target,
                success,
                elapsed_ms: start.elapsed().as_millis(),
                data: Some(PingReport {
                    count: options.count,
                    transmitted,
                    received,
                    attempts,
                    stdout,
                }),
                error: if success {
                    None
                } else if stderr.trim().is_empty() {
                    Some("ping command reported failure".into())
                } else {
                    Some(stderr.trim().to_string())
                },
            }
        }
        Err(err) => ProbeEnvelope {
            target,
            success: false,
            elapsed_ms: start.elapsed().as_millis(),
            data: None,
            error: Some(format!("failed to execute ping: {err}")),
        },
    }
}

fn parse_ping_output(stdout: &str, max_count: u32) -> Vec<PingAttempt> {
    let mut attempts = Vec::new();
    for (idx, line) in stdout.lines().enumerate() {
        if attempts.len() as u32 >= max_count {
            break;
        }
        if line.contains("time=") || line.contains("时间=") || line.contains("Zeit=") {
            let rtt = extract_rtt(line);
            attempts.push(PingAttempt {
                seq: idx as u32,
                success: true,
                rtt_ms: rtt,
                message: line.trim().to_string(),
            });
        }
    }
    if attempts.is_empty() {
        attempts.push(PingAttempt {
            seq: 0,
            success: false,
            rtt_ms: None,
            message: "no success responses parsed".into(),
        });
    }
    attempts
}

fn extract_rtt(line: &str) -> Option<f64> {
    for part in line.split_whitespace() {
        if let Some(value) = part.split('=').nth(1) {
            let value = value.trim_end_matches(|c: char| !c.is_ascii_digit() && c != '.');
            if let Ok(ms) = value.replace(',', ".").parse::<f64>() {
                return Some(ms);
            }
        }
    }
    None
}

pub async fn tcping(options: TcpingOptions<'_>) -> ProbeEnvelope<TcpingReport> {
    let target_display = format!("{}:{}", options.target, options.port);
    let start = Instant::now();

    let mut attempts = Vec::new();
    for seq in 0..options.count {
        let attempt_start = Instant::now();
        let fut = TcpStream::connect((options.target, options.port));
        let result = time::timeout(options.timeout, fut).await;
        match result {
            Ok(Ok(_stream)) => {
                let rtt = attempt_start.elapsed().as_secs_f64() * 1000.0;
                attempts.push(TcpingAttempt {
                    seq,
                    success: true,
                    rtt_ms: Some(rtt),
                    error: None,
                });
            }
            Ok(Err(err)) => {
                attempts.push(TcpingAttempt {
                    seq,
                    success: false,
                    rtt_ms: None,
                    error: Some(err.to_string()),
                });
            }
            Err(_) => {
                attempts.push(TcpingAttempt {
                    seq,
                    success: false,
                    rtt_ms: None,
                    error: Some("timeout".into()),
                });
            }
        }
    }

    let success = attempts.iter().any(|a| a.success);

    ProbeEnvelope {
        target: target_display,
        success,
        elapsed_ms: start.elapsed().as_millis(),
        data: Some(TcpingReport {
            target: options.target.to_string(),
            port: options.port,
            count: options.count,
            attempts,
        }),
        error: if success {
            None
        } else {
            Some("all attempts failed".into())
        },
    }
}

pub async fn dns_lookup(options: DnsOptions<'_>) -> ProbeEnvelope<DnsReport> {
    let start = Instant::now();
    let resolver = TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default());

    let record_type = parse_record_type(options.record_type);

    let response = resolver.lookup(options.query, record_type).await;

    match response {
        Ok(lookup) => {
            let records = lookup
                .iter()
                .map(|r| DnsRecord {
                    record_type: record_type.to_string(),
                    value: r.to_string(),
                })
                .collect::<Vec<_>>();
            ProbeEnvelope {
                target: options.query.to_string(),
                success: true,
                elapsed_ms: start.elapsed().as_millis(),
                data: Some(DnsReport {
                    query: options.query.to_string(),
                    records,
                }),
                error: None,
            }
        }
        Err(err) => ProbeEnvelope {
            target: options.query.to_string(),
            success: false,
            elapsed_ms: start.elapsed().as_millis(),
            data: None,
            error: Some(err.to_string()),
        },
    }
}

fn parse_record_type(input: &str) -> trust_dns_resolver::proto::rr::RecordType {
    use trust_dns_resolver::proto::rr::RecordType;
    match input.to_ascii_uppercase().as_str() {
        "A" => RecordType::A,
        "AAAA" => RecordType::AAAA,
        "MX" => RecordType::MX,
        "TXT" => RecordType::TXT,
        "CNAME" => RecordType::CNAME,
        "NS" => RecordType::NS,
        "SRV" => RecordType::SRV,
        "PTR" => RecordType::PTR,
        _ => RecordType::A,
    }
}

pub async fn portscan(options: PortscanOptions<'_>) -> ProbeEnvelope<PortscanReport> {
    let start = Instant::now();
    let target = sanitize_target(options.target);
    let timeout = options.timeout;
    let mut futures = FuturesUnordered::new();

    for chunk in options.ports.chunks(options.concurrency.max(1)) {
        for &port in chunk {
            futures.push(scan_port(target.clone(), port, timeout));
        }
        time::sleep(Duration::from_millis(1)).await;
    }

    let mut results = Vec::new();
    while let Some(result) = futures.next().await {
        results.push(result);
    }

    results.sort_by_key(|entry| entry.port);
    let open_ports = results
        .iter()
        .filter(|r| r.open)
        .map(|r| r.port)
        .collect::<Vec<_>>();

    ProbeEnvelope {
        target: target.clone(),
        success: true,
        elapsed_ms: start.elapsed().as_millis(),
        data: Some(PortscanReport {
            target,
            ports: results,
            open_ports,
        }),
        error: None,
    }
}

async fn scan_port(target: String, port: u16, timeout: Duration) -> PortStatus {
    let addr = format!("{}:{}", target, port);
    let start = Instant::now();
    let result = time::timeout(timeout, async { TcpStream::connect(addr.clone()).await }).await;

    match result {
        Ok(Ok(_)) => PortStatus {
            port,
            open: true,
            elapsed_ms: start.elapsed().as_millis(),
        },
        _ => PortStatus {
            port,
            open: false,
            elapsed_ms: start.elapsed().as_millis(),
        },
    }
}

pub fn parse_ports(input: &str) -> Result<Vec<u16>, String> {
    let mut ports = Vec::new();
    for segment in input.split(',') {
        let segment = segment.trim();
        if segment.is_empty() {
            continue;
        }
        if let Some((start, end)) = segment.split_once('-') {
            let start = start
                .parse::<u16>()
                .map_err(|_| format!("invalid port: {segment}"))?;
            let end = end
                .parse::<u16>()
                .map_err(|_| format!("invalid port: {segment}"))?;
            if start > end {
                return Err(format!("invalid range {start}-{end}"));
            }
            ports.extend(start..=end);
        } else {
            ports.push(
                segment
                    .parse::<u16>()
                    .map_err(|_| format!("invalid port: {segment}"))?,
            );
        }
    }
    ports.sort_unstable();
    ports.dedup();
    if ports.is_empty() {
        return Err("no ports specified".into());
    }
    Ok(ports)
}

fn decode_output(bytes: &[u8]) -> String {
    if bytes.is_empty() {
        return String::new();
    }
    #[cfg(windows)]
    {
        let (cow, _, _) = GBK.decode(bytes);
        cow.to_string()
    }
    #[cfg(not(windows))]
    {
        String::from_utf8_lossy(bytes).to_string()
    }
}
